Integrating ZK and Spring Web Flow
Henri Chen, Principal Engineer, Potix Corporation
December 3, 2008
Applicable to ZK Spring Integragion Library 1.1RC (zkspring.jar) or later.
- Applicable to ZK 3.5.2 and later.
- Applicable to Spring Web Flow 2.0+
Introduction
This is the third article in a series regarding how to make Spring work with ZK Ajax framework. In the previous articles, this and this, we have discussed about how to secure ZK pages, ZK events, and Spring backend service methods using ZK and Spring Security 2.0 frameworks. In this article, we will demonstrate with an example the way to navigate a work flow with ZK, Spring Security, Spring MVC, and Spring Web Flow.
Spring Web Flow is the module of Spring for implementing flows. Basically, you define your Web page flows with a provided declarative flow definition language in XML configuration files. Then, per the definitions, Spring Web Flow engine transits the pages per the current page and user's action(e.g. when user press the next button). This article focus on illustrating the steps and ways how to configure the Web application and how to design the ZK pages such that ZK and Spring Web Flow work together seamlessly.
Demo
The Example
Same as in the previous articles, I borrowed the booking-faces sample(spring-webflow-2.0.3.RELEASE/projects/spring-webflow-samples/booking-faces/) provided by Spring Web Flow 2.0.3. It is a simple hotel searching and booking application. End users search hotels, pick one, and then book the hotel per the pre-defined work flow. The sample was implemented with JSF pages and I rewrote each JSF .xhtml page to a corresponding ZK .zul page and add necessary ZK libraries and configurations to make it work.
Deploy the war file
Download the zkspringwf.war file and deploy the war file to your servlet container. If you are using Tomcat 6.0, all you have to do is copy the war file to the webapps folder of the Tomcat server and then restart Tomcat. The zkspringwf.war
war file shall deploy and create a new folder named zkspringwf (same name as the file) under the webapps. Now visit http://localhost:8080/zkspringwf/ and you shall see following page:
Configurations
Now lets start walking through the configurations:
/WEB-INF/zk.xml file: the ThreadLocal issue (IMPORTANT!)
The Spring Web Flow engine holds in the servlet thread several ThreadLocal variables for each request so the engine can refer it from time to time. These ThreadLocal variables contains important work flow related information and shall be accessible any time. However, ZK by default spawns a new event thread for each event handling job. That is, the ZK event thread will not have such important ThreadLocal variables and the original assumption is broken.
Therefore, remember always to disable the ZK event thread mechanism entirely when use ZK with Spring. This tells the ZK framework NOT to spawn a new event thread for event handling and everything shall back to it track. That is, configure the /WEB-INF/zk.xml
file as follows:
<zk>
...
<system-config>
<disable-event-thread/>
</system-config>
...
</zk>
/WEB-INF/web.xml
web.xml is the standard servlet configuration file. To make ZK and Spring works, you have to configure this file.
/WEB-INF/web.xml
<web-app ...>
...
<!--
- Spring configurations
-->
<!-- The master configuration file -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/web-application-config.xml
</param-value>
</context-param>
<!-- Enables Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Loads the Spring web application context -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Serves static resource content from .jar files -->
<servlet>
<servlet-name>Resources Servlet</servlet-name>
<servlet-class>org.springframework.js.resource.ResourceServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Resources Servlet</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>
<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
<!--
- ZK configurations
-->
<listener>
<description>
Used to cleanup when a session is destroyed</description>
<display-name>
ZK Session Cleaner</display-name>
<listener-class>org.zkoss.zk.ui.http.HttpSessionListener</listener-class>
</listener>
<!-- ZK loder -->
<servlet>
<description>
ZK loader for ZUML pages</description>
<servlet-name>zkLoader</servlet-name>
<servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class>
<init-param>
<param-name>update-uri</param-name>
<param-value>/zkau</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zul</url-pattern>
</servlet-mapping>
<!-- ZK update engine -->
<servlet>
<description>
The asynchronous update engine for ZK</description>
<servlet-name>auEngine</servlet-name>
<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>auEngine</servlet-name>
<url-pattern>/zkau/*</url-pattern>
</servlet-mapping>
<!-- Default welcome files -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
- Spring configuration:
- The <context-param> tells Spring Framework context loader where to find the context parameter files.
- The <listener>
ContextLoaderListener
defines the Spring Framework context loader that will load and parse the context parameter files. - The <filter>
springSecurityFilterChain
defines the entry servlet filter for Spring Security filter chains. - The <servlet>
Resources Servlet
defines the spring resource servlet which can be used to retrieve resource defined in library .jar files. - The <servlet>
Spring MVC Dispatcher Servlet
defines the entry servlet for Spring Web Flow. Any URL starts with/spring/*
will go through the Spring Web Flow controller
- The most important configuration might be the
Spring MVC Dispatcher Servlet.
It tell the servlet container that all URL that starts with/spring/*
shall be served by this servlet. It is a dispatcher servlet so we will need extra configurations for Spring MVC and Spring Web Flow to bridge the work flow requests to real Spring Web Flow controller.
- ZK Ajax framework configuration:
- The <listener>
HttpSessionListener
is used to cleanup the session when it is destroyed. - The <servlet>
zkLoader
servlet is used to load a ZK page - The <servlet>
auEngine
servlet is used to update a ZK page (Ajax update)
- The <listener>
- This is a typical ZK application configuration. Nothing very special.
- Spring configuration:
/WEB-INF/config/webmvc-config.xml
webmvc-config.xml is the additional configuration file specific to Spring MVC.
/WEB-INF/config/webmvc-config.xml
<beans ...>
<!-- Maps request URIs to controllers -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<value>
/main=flowController
/booking=flowController
</value>
</property>
<property name="defaultHandler">
<!-- Selects view names to render based on the request URI: e.g. /main selects "main" -->
<bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
</property>
</bean>
<!-- Maps logical view names to real page URL (e.g. 'search' to '/WEB-INF/search.zul' -->
<bean id="viewResolver" class="org.zkoss.spring.web.servlet.view.ZkResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<property name="suffix" value=".zul"/>
</bean>
</beans>
- The <bean>
SimpleUrlHandlerMapping
works with Spring MVC will maps the request URI to real Controller. Here all the request URIs in the pattern/main
and/booking
would be dispatched to flowController(which is defined inwebflow-config.xml
configuration file. We will discuss it later). - The <bean>
viewResolver
maps the logical view name returned by controller to real page URL. Note here the name viewResolver has to be given as is.
/WEB-INF/config/webflow-config.xml
webflow-config.xml is the configuration file specific to Spring Web Flow.
/WEB-INF/config/webflow-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:webflow="http://www.springframework.org/schema/webflow-config"
xmlns:zksp="http://www.zkoss.org/2008/zkspring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/webflow-config
http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd
http://www.zkoss.org/2008/zkspring
http://www.zkoss.org/2008/zkspring/zkspring.xsd">
<!-- Executes flows: the central entry point into the Spring Web Flow system -->
<webflow:flow-executor id="flowExecutor">
<webflow:flow-execution-listeners>
<webflow:listener ref="jpaFlowExecutionListener" />
<webflow:listener ref="securityFlowExecutionListener" />
</webflow:flow-execution-listeners>
</webflow:flow-executor>
<!-- Installs a listener that manages JPA persistence contexts for flows that require them -->
<bean id="jpaFlowExecutionListener"
class="org.springframework.webflow.persistence.JpaFlowExecutionListener">
<constructor-arg ref="entityManagerFactory" />
<constructor-arg ref="transactionManager" />
</bean>
<!-- Installs a listener to apply Spring Security authorities -->
<bean id="securityFlowExecutionListener"
class="org.springframework.webflow.security.SecurityFlowExecutionListener" />
<!-- The registry of executable flow definitions -->
<webflow:flow-registry id="flowRegistry"
flow-builder-services="zkFlowBuilderServices">
<webflow:flow-location path="/WEB-INF/flows/main/main.xml" />
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
<!-- Configures the ZK Spring Web Flow integration -->
<zksp:flow-controller id="flowController" flow-executor="flowExecutor"/>
<zksp:flow-builder-services id="zkFlowBuilderServices" />
</beans>
- Spring <webflow:> configuration(in red):
- The
flowExecutor
bean is the central entry point into the Spring Web Flow system. It adds two work flow listeners to handle the JPA data persistence and web flow security protection. - The
flowRegistry
bean is the registry of work flow definitions. It tells the Spring Web Flow system where to find the flow definition file. Also, it specifies whichflow-builder-service
to use for these work flow definitions. Note here it uses zkFlowBuilderServices.
- The
- ZK Spring <zksp:> configuration(in blue):
- The
flowController
bean is the bridge between Spring MVC and ZK Spring Web Flow (flowExecutor). Note the name flowController has to be given as is. - The
zkFlowBuilderServices
bean is where those real web flow services are (expression parsing, view resolving, type conversion, etc.)
- The
- ZK adopts the Spring namespace configuration mechanism so all you need to do to enable the ZK Spring Web Flow Integration is by specifying these two beans.
Define a work flow
After finishing application configuration, it is time to define the work flow.
/WEB-INF/flows/main/main.xml
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<var name="searchCriteria" class="org.springframework.webflow.samples.booking.SearchCriteria" />
<view-state id="enterSearchCriteria">
<on-render>
<evaluate expression="bookingService.findBookings(currentUser.name)" result="viewScope.bookings" />
</on-render>
<transition on="search" to="reviewHotels">
<evaluate expression="searchCriteria.resetPage()" />
</transition>
<transition on="cancelBooking">
<evaluate expression="bookingService.cancelBooking(componentScope.booking)" />
<render fragments="bookingsFragment" />
</transition>
</view-state>
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" />
</on-render>
<transition on="sort">
<set name="searchCriteria.sortBy" value="requestParameters.sortBy" />
<render fragments="searchResultsFragment" />
</transition>
<transition on="previous">
<evaluate expression="searchCriteria.previousPage()" />
<render fragments="searchResultsFragment" />
</transition>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
<render fragments="searchResultsFragment" />
</transition>
<transition on="select" to="reviewHotel">
<set name="flowScope.hotel" value="self.attributes.hotel" />
</transition>
<transition on="changeSearch" to="changeSearchCriteria" />
</view-state>
<view-state id="reviewHotel">
<transition on="book" to="bookHotel" />
<transition on="cancel" to="enterSearchCriteria" />
</view-state>
<subflow-state id="bookHotel" subflow="booking">
<input name="hotelId" value="hotel.id" />
<transition on="bookingConfirmed" to="finish" />
<transition on="bookingCancelled" to="enterSearchCriteria" />
</subflow-state>
<view-state id="changeSearchCriteria" view="enterSearchCriteria" popup="true">
<on-entry>
<render fragments="hotelSearchFragment" />
</on-entry>
<transition on="search" to="reviewHotels">
<evaluate expression="searchCriteria.resetPage()"/>
</transition>
</view-state>
<end-state id="finish" />
</flow>
A work flow is defined in a separate XML configuration file. A work flow basically is composed of states. Inside each state, we can defined transition to tell the flow engine which state to go on some predefined action event. Also, you can do some evaluate in the life cycle of the work flow. However, I am not going to explain every tag used in this work flow definition file. I will focus on those tags that related to the mapping web pages. If you need to know more details, you can download the spring-webflow-reference.pdf from the Spring website.
- <view-state>: define the view state. Each
view-state
is mapped to a real web page. The view resolver configured in zkFlowBuilderServices bean(configured in webflow-config.xml) is used to resolve the view-state id into a Spring MVC view name - <transition>: define the transition condition. The attribute on=action-event" tells when to trigger a flow transition, and the optional attribute to="target-state" tells which target state to go when the specified transition action event is fired. If attribute to is omitted, flow engine will keep in this state. In such case, there shall be generally at least an
<evaluate>
tag is specified. As for how to fire a transition action event in the zul page, I will explain later with an example zul page. - <evaluate:>: define the evaluation expression. Each
<evaluate>
tag request to evaluate the specified expression as defined in attribute expression. These expression can be evaluated as the Unified Epression Language(EL). Note that you can use all ZK implicit objects and accessible variables here including self, event, desktop, page, and so on. That is, you can write ZK page controll code in the flow definition file if you like.
- <view-state>: define the view state. Each
Define an associated view-state page
Here we discuss the associatation between a zul page and a view state.
/WEB-INF/flows/main/enterSearchCriteria.zul
<?page title="ZK Spring: Hotel Booking Sample Application" complete="true" ?>
<?init class="org.zkoss.zk.ui.util.Composition" arg0="/WEB-INF/layouts/standard.zul"?>
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" arg0="./hotelSearch"?>
<?variable-resolver class="org.zkoss.spring.DelegatingVariableResolver"?>
<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c" ?>
<zk:zk xmlns="http://www.zkoss.org/2005/zk/native"
xmlns:zul="http://www.zkoss.org/2005/zul"
xmlns:zk="http://www.zkoss.org/2005/zk">
<zul:div id="hotelSearch" class="section" self="@{define(content) fragment(hotelSearchFragment)}">
<span class="errors">
<!-- <h:messages globalOnly="true" /> -->
</span>
<h2>Search Hotels</h2>
<form id="mainForm">
<fieldset>
<div class="field">
<div class="label">
Search String:
</div>
<div class="input">
<zul:textbox id="searchString" value="@{searchCriteria.searchString}"
tooltiptext="Search hotels by name, address, city, or zip."/>
</div>
</div>
<div class="field">
<div class="label">
Maximum results:
</div>
<div class="input">
<zul:listbox id="pageSize" rows="1" mold="select" selectedItem="@{searchCriteria.pageSize}">
<zul:listitem forEach="" value="0" label=""/>
</zul:listbox>
</div>
</div>
<div class="buttonGroup">
<zul:button self="@{action(search)}" label="Find Hotels" onClick=""/>
</div>
</fieldset>
</form>
</zul:div>
...
</zk:zk>
Per the file name, you might have guessed this example view state page is associated to the view state enterSearchCriteria. Spring Work Flow maps a view state to a view state page URL by name convention. It is a combination of the view state id and the URL path of the work flow definition file.
Following is some important notes regarding a ZK view state page:
The <?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ...?>
is required. ZK Spring Web Flow Integration assumes it. Basically, any settable work flow variables shall be specified as annotated data binding. E.g. the @{searchCriteria.searchString} and the @{searchCriteria.pageSize} in this example page.- The
<?variable-resolver class="org.zkoss.spring.DelegatingVariableResolver"?>
is a must. ZK rendering engine and data binder use this resolver to resove work flow variables defined in the work flow configuration file. - The
<zul:button self="@{action(search)}" .../>
is how you trigger a view state transition. Simply specify a component action annotation in the form of @{action(action-event)} and you are done; where the action-event is the transition action event name. That is, in this example, when an end user click this button, it tells Spring Web Flow engine that a transition action event search is fired for the current view state. Per the transition definition,<transition on="search" to="reviewHotels">
, the Spring Web Flow engine will transit view state page from current enterSearchCriteria view state to the reviewHotels view state.
Summary
The Spring Web Flow apparently is designed with page based navigation in mind. It provides some minor Ajax effects (e.g. Popup and Fragment) and that is all. Then what role can a rich Ajax framework like ZK play? A good practice might be that doing fine grain Ajax operations in a page with ZK event programming whilst doing coarse work flow page transitions with Spring Web Flow definition. However, you can also define <transition>
without the to attribute and make it doing <evalute>
only so it would work like a pure ZK event listener. It is all up to you and your applications' requirement.
We now have made Spring Web Flow work with ZK. The next step shall be focused on enabling injection of ZK components into Spring beans. Integrating different frameworks together has not been an easy job and I might miss something. We welcome your feedback and suggestions so we can make ZK Spring Integration better.
Download
Download the example codes(.war file).
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |